package com.dahuyou.asm.callOuterCls;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.AdviceAdapter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;

import static org.objectweb.asm.Opcodes.ASM5;

public class CallOuterMethodEnhancer extends ClassLoader {

    public static void main(String[] args) throws Exception {
        // 读取要插桩加强的类
        ClassReader cr = new ClassReader(PayController.class.getName());
        // 准备往要插桩加强的类中写内容
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        // 准备插桩
        ClassVisitor cv = new ProfilingClassAdapter(cw, PayController.class.getSimpleName());
        // 正式插桩
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        // 获取插桩后的代码
        byte[] bytes = cw.toByteArray();

        // 反射执行插桩后的字节码
        Class<?> clazz = new CallOuterMethodEnhancer().defineClass("com.dahuyou.asm.callOuterCls.PayController", bytes, 0, bytes.length);
        // 反射获取 main 方法
        Method method = clazz.getMethod("pay", int.class, int.class);
        Object obj = method.invoke(clazz.newInstance(), 69089, 285);
        System.out.println("结果：" + obj);

        outputClazz(bytes);
    }

    static class ProfilingClassAdapter extends ClassVisitor {

        public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) {
            super(ASM5, cv);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            if (!"pay".equals(name)) return super.visitMethod(access, name, descriptor, signature, exceptions);
            MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
            return new ProfilingMethodVisitor(mv, access, name, descriptor);
        }
    }

    static class ProfilingMethodVisitor extends AdviceAdapter {

        private String name;

        protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(ASM5, methodVisitor, access, name, descriptor);
            this.name = name;
        }

        @Override
        public void visitVarInsn(int opcode, int var) {
            super.visitVarInsn(opcode, var);
        }

        @Override
        public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
            super.visitFieldInsn(opcode, owner, name, descriptor);
        }

        /**
         * 实现效果：
         *     public void pay(int userId, int payAmount) {
         *         AuditLogUtil.infoLog("pay", new int[] {userId, payAmount});
         *         System.out.println("用户：" + userId + ", 调用支付系统完成支付" + payAmount + "，准备发货！");
         *         return;
         *     }
         * 其中AuditLogUtil.infoLog("pay", new int[] {userId, payAmount});就是要插桩的代码
         */
        @Override
        protected void onMethodEnter() {
            // ldc 加载方法名称常量
            mv.visitLdcInsn(name); // 方法名称压到栈顶 此时栈：pay
            mv.visitInsn(ICONST_2); // 将int型2推送至栈顶 此时栈：2, pay
            mv.visitIntInsn(NEWARRAY, T_INT); // 获取栈顶元素，并以其为长度创建一个数组，并将其引用压倒栈顶 此时栈：new int[]{}, pay
            mv.visitInsn(DUP); // 复制栈顶元素并压到栈顶 此时栈：new int[], new int[], pay
            mv.visitInsn(ICONST_0); // 将常量0压到栈顶 此时栈：0, new int[], new int[], pay
            mv.visitVarInsn(ILOAD, 1); // 将本地变量表1位置变量压倒栈顶  1位置变量, 此时栈：0, new int[], new int[], pay
            mv.visitInsn(IASTORE); // 将栈顶int型数值存入指定数组的指定索引位置 new int[0] = 1位置变量，此时栈new int[], pay
            mv.visitInsn(DUP); // 复制栈顶元素 此时栈：new int[], new int[], pay
            mv.visitInsn(ICONST_1); // 加载常量1 此时栈：1, new int[], new int[], pay
            mv.visitVarInsn(ILOAD, 2); // 加载本地变量表slot 2变量 此时栈：2位置变量, 1, new int[], new int[], pay
            mv.visitInsn(IASTORE); // 栈顶元素存储到数组 new int[1] = 2位置变量 此时栈：new int[], pay
            mv.visitMethodInsn(INVOKESTATIC, "com/dahuyou/asm/callOuterCls/AuditLogUtil", "infoLog", "(Ljava/lang/String;[I)V", false); // 调用静态方法infoLog，参数为当前栈的new int[], pay，完成打印
        }
    }

    private static void outputClazz(byte[] bytes) {
        // 输出类字节码
        FileOutputStream out = null;
        try {
            String pathName = CallOuterMethodEnhancer.class.getResource("/").getPath() + "AsmCallOuterMethodEnhancer.class";
            out = new FileOutputStream(new File(pathName));
            System.out.println("ASM类输出路径：" + pathName);
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
